{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "bSEcFiPijRcb"
   },
   "source": [
    "# Time Series Simulation with Multiple Loads and Generators\n",
    "\n",
    "This notebook is an extension of the `time_series.ipynb` tutorial, focusing on the simulation of multiple loads and generators using the timeseries and control modules in pandapower. In this advanced tutorial, we will demonstrate how to perform a time series simulation that includes:\n",
    "\n",
    "* Multiple loads with varying active and reactive power demands.\n",
    "* Multiple generators with varying power outputs.\n",
    "* The use of `ConstControl` to dynamically set the active and reactive power values for loads and generators based on a time series data source.\n",
    "\n",
    "A time series calculation requires the following inputs:\n",
    "* A pandapower network (`net`).\n",
    "* The time series data (in a pandas DataFrame, for example).\n",
    "\n",
    "By the end of this tutorial, you will be able to set up and run a time series simulation on any arbitrary network in pandapower that accurately models the behavior of a power network with multiple loads and generators over time."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "YquzqXkKjRcd"
   },
   "source": [
    "First we need some imports. Specific for this example are:\n",
    "* ConstControl -> \"constant\" controllers, which change the P and Q values of sgens and loads\n",
    "* DFData -> The Dataframe Datasource. This Dataframe holds the time series to be calculated\n",
    "* OutputWriter -> The output writer, which is required to write the outputs to the hard disk\n",
    "* run_timeseries -> the \"main\" time series function, which basically calls the controller functions (to update the P, Q of the ConstControllers) and runpp."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "Dx9ZKvbfjRcg"
   },
   "outputs": [],
   "source": [
    "import os\n",
    "import pandas as pd\n",
    "import tempfile\n",
    "import random\n",
    "from pandapower.timeseries import DFData, OutputWriter, run_timeseries\n",
    "from pandapower.control import ConstControl\n",
    "from pandapower.networks.power_system_test_cases import case14\n",
    "from pandapower.run import runpp\n",
    "\n",
    "random.seed(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "9CcyC2mgjRc_"
   },
   "source": [
    "## Function Documentation: `create_data_source`\n",
    "\n",
    "### Description\n",
    "The `create_data_source` function generates a time series data source for a given pandapower network. It creates a pandas DataFrame containing the active and reactive power values for loads and generators over a specified number of timesteps. The function also introduces random variations to these values based on a scaling factor.\n",
    "\n",
    "### Parameters\n",
    "- `net` (pandapowerNet): The pandapower network object containing the buses, loads, and generators.\n",
    "- `scale` (float): A scaling factor used to introduce random variations to the power values.\n",
    "- `n_timesteps` (int, optional): The number of timesteps for which the time series data is generated. Default is 24.\n",
    "\n",
    "### Returns\n",
    "- `profiles` (pandas.DataFrame): A DataFrame containing the time series data for active and reactive power values of loads and generators.\n",
    "- `ds` (DFData): A DataFrame DataSource object that can be used with pandapower controllers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "KVrcajmqjRdB"
   },
   "outputs": [],
   "source": [
    "def create_data_source(net, scale, n_timesteps):\n",
    "    profiles = pd.DataFrame()\n",
    "\n",
    "    for each_time_step in range(n_timesteps):\n",
    "        # this loop is to create the profiles for each time step\n",
    "        for index, row in net.bus.iterrows():\n",
    "            # this loop is to create the load profiles for each bus having a load connected\n",
    "            if row['name'] in net.load['bus'].values:\n",
    "                p_mw = net.load.loc[net.load['bus'] == row['name'], ['p_mw', 'q_mvar']].values[0][0]\n",
    "                q_mvar = net.load.loc[net.load['bus'] == row['name'], ['p_mw', 'q_mvar']].values[0][1]\n",
    "                if p_mw is not None: # if the load has a p_mw value\n",
    "                    p_mw = p_mw + scale*random.random()*p_mw\n",
    "                if q_mvar is not None: # if the load has a q_mvar value\n",
    "                    q_mvar = q_mvar + scale*random.random()*q_mvar\n",
    "                # add the load values to the profiles dataframe\n",
    "                profiles.loc[each_time_step, str(row['name']) + '_load_p'] = p_mw\n",
    "                profiles.loc[each_time_step, str(row['name']) + '_load_q'] = q_mvar\n",
    "        # this loop is to create the generation profiles for each bus having a generator connected\n",
    "        for index, row in net.bus.iterrows():\n",
    "            if row['name'] in net.gen['bus'].values:\n",
    "                p_mw = net.gen.loc[net.gen['bus'] == row['name'], ['p_mw']].values[0]\n",
    "                if p_mw is not None:\n",
    "                    p_mw = p_mw + scale*random.random()*p_mw\n",
    "                profiles.loc[each_time_step, str(row['name']) + '_gen_p'] = p_mw\n",
    "            \n",
    "    ds = DFData(profiles) # create a data source from the profiles dataframe\n",
    "    return profiles, ds"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `create_controllers`\n",
    "\n",
    "This function creates constant controllers for loads and generators in a given network using a specified data source.\n",
    "\n",
    "#### Parameters:\n",
    "- `net`: The network object containing the loads and generators.\n",
    "- `ds`: The data source object that provides the profiles for the loads and generators.\n",
    "\n",
    "#### Functionality:\n",
    "1. Iterates over each row in the `net.load` DataFrame:\n",
    "   - Creates a constant controller for the active power (`p_mw`) of the load.\n",
    "   - Creates a constant controller for the reactive power (`q_mvar`) of the load.\n",
    "2. Iterates over each row in the `net.gen` DataFrame:\n",
    "   - Creates a constant controller for the active power (`p_mw`) of the generator."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "C7jNYZYhjRdN"
   },
   "outputs": [],
   "source": [
    "def create_controllers(net, ds):\n",
    "    for index, row in net.load.iterrows(): # create a controller for each load    \n",
    "        ConstControl(net, element='load', variable='p_mw', element_index=[index],\n",
    "                    data_source=ds, profile_name=[str(row['bus']) + '_load_p'])\n",
    "        ConstControl(net, element='load', variable='q_mvar', element_index=[index],\n",
    "                    data_source=ds, profile_name=[str(row['bus']) + '_load_q'])\n",
    "    \n",
    "    for index, row in net.gen.iterrows(): # create a controller for each generator\n",
    "        ConstControl(net, element='gen', variable='p_mw', element_index=[index],\n",
    "                    data_source=ds, profile_name=[str(row['bus']) + '_gen_p'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "kDA6xHDWjRdT"
   },
   "source": [
    "### `create_output_writer`\n",
    "\n",
    "This function creates an `OutputWriter` object for logging specific variables during a time series simulation.\n",
    "\n",
    "#### Parameters:\n",
    "- `net`: The network object containing the elements to be logged.\n",
    "- `time_steps`: The number of time steps for the simulation.\n",
    "- `output_dir`: The directory where the output files will be saved.\n",
    "\n",
    "#### Returns:\n",
    "- `ow`: An `OutputWriter` object configured to log specified variables.\n",
    "\n",
    "#### Functionality:\n",
    "1. Initializes an `OutputWriter` object with the given network, time steps, and output directory.\n",
    "2. Logs the following variables to be saved during the time series loop:\n",
    "   - Active power (`p_mw`) of loads (`res_load`).\n",
    "   - Voltage magnitude (`vm_pu`) of buses (`res_bus`).\n",
    "   - Loading percentage (`loading_percent`) of lines (`res_line`).\n",
    "   - Current (`i_ka`) of lines (`res_line`).\n",
    "   - Active power (`p_mw`) of generators (`res_gen`).\n",
    "   - Reactive power (`q_mvar`) of generators (`res_gen`).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "GlttGzEHjRdV"
   },
   "outputs": [],
   "source": [
    "def create_output_writer(net, time_steps, output_dir):\n",
    "    ow = OutputWriter(net, time_steps, output_path=output_dir, output_file_type=\".xlsx\", log_variables=[])\n",
    "    # these variables are saved to the harddisk after / during the time series loop\n",
    "    ow.log_variable('res_load', 'p_mw')\n",
    "    ow.log_variable('res_bus', 'vm_pu')\n",
    "    ow.log_variable('res_line', 'loading_percent')\n",
    "    ow.log_variable('res_line', 'i_ka')\n",
    "    ow.log_variable('res_gen', 'p_mw')\n",
    "    ow.log_variable('res_gen', 'q_mvar')\n",
    "    return ow"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "DAIvq_bUjRdb"
   },
   "source": [
    "Now lets execute the code."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "wTlSALR0jRdd",
    "outputId": "959014ba-8db7-412b-d425-f2af199207f0"
   },
   "outputs": [],
   "source": [
    "output_dir = os.path.join(tempfile.gettempdir(), \"time_series_example\")\n",
    "print(\"Results can be found in your local temp folder: {}\".format(output_dir))\n",
    "if not os.path.exists(output_dir):\n",
    "    os.mkdir(output_dir)\n",
    "\n",
    "# load ieee 14 bus system\n",
    "net = case14()\n",
    "\n",
    "# run load flow and save results\n",
    "runpp(net)\n",
    "\n",
    "n_timesteps = int(24*60/5) # number of timesteps to be calculated (5 minutes resolution for 24 hours)\n",
    "scale = 0.5 # scaling factor for the load and generation profiles\n",
    "\n",
    "profiles, ds = create_data_source(net, scale, n_timesteps) # create profiles and data source\n",
    "\n",
    "# 3. create controllers (to control P values of the load and the sgen)\n",
    "create_controllers(net, ds)\n",
    "\n",
    "# time steps to be calculated. Could also be a list with non-consecutive time steps\n",
    "time_steps = range(0, n_timesteps)\n",
    "\n",
    "# 4. the output writer with the desired results to be stored to files.\n",
    "ow = create_output_writer(net, time_steps, output_dir=output_dir)\n",
    "\n",
    "\n",
    "# 5. the main time series function\n",
    "run_timeseries(net, time_steps) # the main time series function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(net.res_line.loading_percent) # print the loading percent of the lines"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "v8-ZYJFujRdk"
   },
   "source": [
    "If everything works you should have the desired results the output_folder, which is the temporary folder of your operating system (see print statement above)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "output_dir"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "EzTC_NnRjRdn"
   },
   "source": [
    "## Plot the result\n",
    "Let's read the result from the disk and plot it"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "DhVd7-3ejRdp",
    "outputId": "101d40f1-78fc-485d-eac9-70b90baf3abf"
   },
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline  \n",
    "\n",
    "x_label = \"time step\"\n",
    "# voltage results\n",
    "vm_pu_file = os.path.join(output_dir, \"res_bus\", \"vm_pu.xlsx\")\n",
    "vm_pu = pd.read_excel(vm_pu_file, index_col=0)\n",
    "vm_pu.plot(label=\"vm_pu\")\n",
    "plt.xlabel(x_label)\n",
    "plt.ylabel(\"voltage mag. [p.u.]\")\n",
    "plt.title(\"Voltage Magnitude\")\n",
    "plt.grid()\n",
    "plt.show()\n",
    "\n",
    "# line loading results\n",
    "ll_file = os.path.join(output_dir, \"res_line\", \"loading_percent.xlsx\")\n",
    "line_loading = pd.read_excel(ll_file, index_col=0)\n",
    "line_loading.plot(label=\"line_loading\")\n",
    "plt.xlabel(x_label)\n",
    "plt.ylabel(\"line loading [%]\")\n",
    "plt.title(\"Line Loading\")\n",
    "plt.grid()\n",
    "plt.show()\n",
    "\n",
    "# load results\n",
    "load_file = os.path.join(output_dir, \"res_load\", \"p_mw.xlsx\")\n",
    "load = pd.read_excel(load_file, index_col=0)\n",
    "load.plot(label=\"load\")\n",
    "plt.xlabel(x_label)\n",
    "plt.ylabel(\"P [MW]\")\n",
    "plt.grid()\n",
    "plt.show()\n",
    "\n",
    "# generation results [p_mw]\n",
    "gen_file = os.path.join(output_dir, \"res_gen\", \"p_mw.xlsx\")\n",
    "gen = pd.read_excel(gen_file, index_col=0)\n",
    "gen.plot(label=\"gen\")\n",
    "plt.xlabel(x_label)\n",
    "plt.ylabel(\"P [MW]\")\n",
    "plt.grid()\n",
    "plt.show()\n",
    "\n",
    "# generation results [q_mvar]\n",
    "gen_file = os.path.join(output_dir, \"res_gen\", \"q_mvar.xlsx\")\n",
    "gen = pd.read_excel(gen_file, index_col=0)\n",
    "gen.plot(label=\"gen\")\n",
    "plt.xlabel(x_label)\n",
    "plt.ylabel(\"Q [Mvar]\")\n",
    "plt.grid()\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "colab": {
   "name": "time_series.ipynb",
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
